@abstractframework/continuum 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -19
- package/bin/cli.js +157 -32
- package/bin/hub_proxy.js +37 -17
- package/bin/settings.js +511 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-CS1wi4Y3.js → abnfDiagram-VRR7QNED-DkP5u0Ku.js} +2 -2
- package/dist/assets/{arc-DYoB4bu2.js → arc-vxPq5BjA.js} +2 -2
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-DoNVXIHa.js → architectureDiagram-ZJ3FMSHR-ku0NT7zM.js} +2 -2
- package/dist/assets/{blockDiagram-677ZJIJ3-CeP5oeQE.js → blockDiagram-677ZJIJ3-DXQQcNdo.js} +2 -2
- package/dist/assets/{c4Diagram-LMCZKHZV-BEGwHMwG.js → c4Diagram-LMCZKHZV-DuZ33cED.js} +2 -2
- package/dist/assets/channel-tvH0KUyT.js +2 -0
- package/dist/assets/{chunk-2Q5K7J3B-jW9ElN5d.js → chunk-2Q5K7J3B-BGWsS7uu.js} +2 -2
- package/dist/assets/{chunk-32BRIVSS-BS6et1PP.js → chunk-32BRIVSS-BgokockY.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DQ9hLs-B.js → chunk-5VM5RSS4-OLY5jTMg.js} +2 -2
- package/dist/assets/{chunk-EX3LRPZG-Wp68VufQ.js → chunk-EX3LRPZG-BWHQVZSz.js} +2 -2
- package/dist/assets/{chunk-JWPE2WC7-DYSRWrFH.js → chunk-JWPE2WC7-CuM7Hcrw.js} +2 -2
- package/dist/assets/{chunk-MOJQB5TN-8cwmjmPr.js → chunk-MOJQB5TN-DI56DwNK.js} +2 -2
- package/dist/assets/{chunk-RYQCIY6F--F4n2CLl.js → chunk-RYQCIY6F-phvbPmeC.js} +2 -2
- package/dist/assets/{chunk-V7JOEXUC-vx1HHblG.js → chunk-V7JOEXUC-Cv7eW1t4.js} +2 -2
- package/dist/assets/{chunk-VR4S4FIN-BWDfVdfu.js → chunk-VR4S4FIN-D8k2SzA3.js} +2 -2
- package/dist/assets/{chunk-XXDRQBXY-tmf9giTR.js → chunk-XXDRQBXY-DZy8Plz8.js} +2 -2
- package/dist/assets/classDiagram-OUVF2IWQ-C0x6Ou39.js +2 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-C0x6Ou39.js +2 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-D4pA4Uis.js → cose-bilkent-JH36ORCC-CiwWaVUb.js} +2 -2
- package/dist/assets/{cynefin-VYW2F7L2-fqThC73D.js → cynefin-VYW2F7L2-Cz79Fwdx.js} +2 -2
- package/dist/assets/{cynefinDiagram-TSTJHNR4-xKZSOndK.js → cynefinDiagram-TSTJHNR4-DYlkii-x.js} +2 -2
- package/dist/assets/{dagre-VKFMJZFB-DckpxAkb.js → dagre-VKFMJZFB-Cumo7o1R.js} +2 -2
- package/dist/assets/{diagram-FQU43EPY-GJNsMViq.js → diagram-FQU43EPY-Dorm69_e.js} +2 -2
- package/dist/assets/{diagram-G47NLZAW-BAx3mWlA.js → diagram-G47NLZAW-Bhtzc4JU.js} +2 -2
- package/dist/assets/{diagram-NH7WQ7WH-BvAd2Vow.js → diagram-NH7WQ7WH-DJF-iHCK.js} +2 -2
- package/dist/assets/{diagram-OA4YK3LP-DxDd3CWT.js → diagram-OA4YK3LP-8mQ4915-.js} +2 -2
- package/dist/assets/{diagram-WEI45ONY-C3s99ME6.js → diagram-WEI45ONY-C4Bv6dbm.js} +2 -2
- package/dist/assets/{ebnfDiagram-CCIWWBDH-DwY_zzL5.js → ebnfDiagram-CCIWWBDH-DzagnQVX.js} +2 -2
- package/dist/assets/{erDiagram-Q63AITRT-ByyDRAIk.js → erDiagram-Q63AITRT-BbkYElA6.js} +2 -2
- package/dist/assets/{flowDiagram-23GEKE2U-nzwsYkxG.js → flowDiagram-23GEKE2U-OcR4vkjk.js} +2 -2
- package/dist/assets/{ganttDiagram-NO4QXBWP-xjthVT1C.js → ganttDiagram-NO4QXBWP-DyAPiIdB.js} +2 -2
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-BhwlJJX_.js → gitGraphDiagram-IHSO6WYX-pymEefZi.js} +2 -2
- package/dist/assets/{index-B90p5t5R.js → index-DTn9k-3t.js} +48 -48
- package/dist/assets/{infoDiagram-FWYZ7A6U-lgB-aUlU.js → infoDiagram-FWYZ7A6U-Dh41Som-.js} +2 -2
- package/dist/assets/{ishikawaDiagram-FXEZZL3T--cMGxagK.js → ishikawaDiagram-FXEZZL3T-Xc9vuvPV.js} +2 -2
- package/dist/assets/{journeyDiagram-5HDEW3XC-ZGXAX-J5.js → journeyDiagram-5HDEW3XC-bJ0gK8bC.js} +2 -2
- package/dist/assets/{kanban-definition-HUTT4EX6-Y4tnA7YW.js → kanban-definition-HUTT4EX6-Cu1Zn3KW.js} +2 -2
- package/dist/assets/{linear-DNPBmvUz.js → linear-DrGsraGo.js} +2 -2
- package/dist/assets/{mermaid.core-WQuHCSR5.js → mermaid.core-CkQqPHPZ.js} +5 -5
- package/dist/assets/{mindmap-definition-LN4V7U3C-BqOKjeA8.js → mindmap-definition-LN4V7U3C-ZCEP6XWH.js} +2 -2
- package/dist/assets/{pegDiagram-2B236MQR-DoKnv3Ez.js → pegDiagram-2B236MQR-DjDrn07n.js} +2 -2
- package/dist/assets/{pieDiagram-ENE6RG2P-CK7u0YoT.js → pieDiagram-ENE6RG2P-oD3ciEy7.js} +2 -2
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-pZ2BrTn4.js → quadrantDiagram-ABIIQ3AL-CYbDYNM7.js} +2 -2
- package/dist/assets/{railroadDiagram-RFXS5EU6-B8uTEagG.js → railroadDiagram-RFXS5EU6-DMcO7O0d.js} +2 -2
- package/dist/assets/{requirementDiagram-TGXJPOKE-vkNASsjm.js → requirementDiagram-TGXJPOKE-DeeSIVn-.js} +2 -2
- package/dist/assets/{sankeyDiagram-HTMAVEWB-Bhql_cld.js → sankeyDiagram-HTMAVEWB-C43xzNvP.js} +2 -2
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-CHd_hwoo.js → sequenceDiagram-DBY2YBRQ-d3lj6z8y.js} +2 -2
- package/dist/assets/{sizeCapture-X5ZJPWSS-DP6WJWOP.js → sizeCapture-X5ZJPWSS-D4UyeLsX.js} +2 -2
- package/dist/assets/{stateDiagram-2N3HPSRC-DJtFU8bY.js → stateDiagram-2N3HPSRC-Cq4YMMP1.js} +2 -2
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DMVYrQgX.js +2 -0
- package/dist/assets/{swimlanes-5IMT3BWC-6-t4XS8A.js → swimlanes-5IMT3BWC-CRbkVLoI.js} +3 -3
- package/dist/assets/swimlanesDiagram-G3AALYLV-Bzg2g7oq.js +9 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-8natOnn8.js → timeline-definition-FHXFAJF6-BN8oLdvs.js} +2 -2
- package/dist/assets/{vennDiagram-L72KCM5P-BgwY6aTt.js → vennDiagram-L72KCM5P-BqAHS4HE.js} +2 -2
- package/dist/assets/{wardleyDiagram-EHGQE667-BRA-hXqX.js → wardleyDiagram-EHGQE667-D2aA14yL.js} +2 -2
- package/dist/assets/{xychartDiagram-FW5EYKEG-Bps4vnDY.js → xychartDiagram-FW5EYKEG-BkrnnKWS.js} +2 -2
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/dist/assets/channel-CGvEfjJ_.js +0 -2
- package/dist/assets/classDiagram-OUVF2IWQ-Bs-9E6z7.js +0 -2
- package/dist/assets/classDiagram-v2-EOCWNBFH-Bs-9E6z7.js +0 -2
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DmsSVvZs.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-DVV8zrk-.js +0 -9
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ cockpit (board-first, kanban/scrum-inspired — see
|
|
|
18
18
|
- **Backlog** — the full catalog power view (all kinds incl. recurrent /
|
|
19
19
|
deprecated / trash, search, batch execute, merge, AI assist + advisor).
|
|
20
20
|
- **Agents & Entities** — the workforce: the configured execution agent
|
|
21
|
-
(
|
|
21
|
+
(Codex CLI, Claude Code, Cursor Agent or AbstractCode — a gateway
|
|
22
|
+
setting), the advisor agent, a
|
|
22
23
|
track record from execution history, and gateway entities with their
|
|
23
24
|
skills.
|
|
24
25
|
- **Team** — an [agora](docs/configuration.md#team-page-agora-hub) hub
|
|
@@ -32,7 +33,11 @@ cockpit (board-first, kanban/scrum-inspired — see
|
|
|
32
33
|
- **Services** — control managed prod/UAT processes (start / stop /
|
|
33
34
|
restart / redeploy) and write-only environment variables. This surface is
|
|
34
35
|
high trust: see [SECURITY.md](SECURITY.md) before exposing it.
|
|
35
|
-
- **Settings** — gateway
|
|
36
|
+
- **Settings** — gateway sign-in, execution defaults (UAT or inplace), AI
|
|
37
|
+
and voice preferences, and **Gateway administration**: the backlog
|
|
38
|
+
folder, exec runner, executor and process manager, each shown with where
|
|
39
|
+
its value comes from (launch flag, setting, environment (legacy), or
|
|
40
|
+
default). Only a gateway admin can change them.
|
|
36
41
|
|
|
37
42
|
Executing a Ready item is gated by an advisory **Definition of Ready**
|
|
38
43
|
checklist (type, real summary, acceptance criteria, test commands — parsed
|
|
@@ -41,18 +46,20 @@ review shows the acceptance criteria as a **Definition of Done** checklist.
|
|
|
41
46
|
|
|
42
47
|
The observer app ([AbstractObserver](https://github.com/lpalbou/AbstractObserver))
|
|
43
48
|
watches and discusses the running system; this app develops and deploys it.
|
|
44
|
-
The pipeline is executor-agnostic
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
(see [docs/architecture.md](docs/architecture.md)).
|
|
49
|
+
The pipeline is executor-agnostic: the gateway's `executor` setting picks
|
|
50
|
+
the agent that runs items (`codex` by default; also `claude`,
|
|
51
|
+
`cursor-agent`, `abstractcode`), and the UI reads the executor identity from
|
|
52
|
+
the gateway's exec config (see [docs/architecture.md](docs/architecture.md)).
|
|
48
53
|
|
|
49
54
|
## Install and run
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
Requirements:
|
|
57
|
+
|
|
58
|
+
- Node.js ≥ 18.
|
|
59
|
+
- **AbstractGateway 0.4.1 or newer** (the supported gateway; default
|
|
60
|
+
`http://127.0.0.1:8080`). See
|
|
61
|
+
[AbstractGateway](https://github.com/lpalbou/AbstractGateway). Against an
|
|
62
|
+
older gateway the Board shows its *folder not available* panel.
|
|
56
63
|
|
|
57
64
|
```bash
|
|
58
65
|
npx @abstractframework/continuum
|
|
@@ -65,14 +72,25 @@ Open `http://localhost:3002` and sign in with a gateway user and token (the
|
|
|
65
72
|
connection badge at the bottom of the sidebar). The token is exchanged
|
|
66
73
|
server-side for an HttpOnly session cookie and never stored in the browser.
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
The server is configured with launch flags or saved settings (full list in
|
|
69
76
|
[docs/configuration.md](docs/configuration.md), or `abstractcontinuum --help`):
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
```bash
|
|
79
|
+
abstractcontinuum --port 3002 --gateway-url http://127.0.0.1:8080 --hub-seat alice
|
|
80
|
+
abstractcontinuum config set hub_seat alice # saved in ~/.abstractcontinuum/settings.json
|
|
81
|
+
abstractcontinuum config get # every setting and where it comes from
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `--port` (default `3002`), `--host` (default `127.0.0.1`)
|
|
85
|
+
- `--gateway-url` — the gateway this deployment talks to (default
|
|
86
|
+
`http://127.0.0.1:8080`)
|
|
87
|
+
- `--hub-url` / `--hub-seat` / `--hub-token-file` — the agora hub, your seat
|
|
88
|
+
and its key for the Team page. The hub seat is also on the Settings page.
|
|
89
|
+
|
|
90
|
+
A fresh gateway needs no backlog setup: it keeps its own backlog folder, and
|
|
91
|
+
the Board opens on **Your backlog is empty** with **Create your first item**.
|
|
92
|
+
To point the Board at a project, or to enable executions, see
|
|
93
|
+
[docs/getting-started.md](docs/getting-started.md#your-backlog).
|
|
76
94
|
|
|
77
95
|
Before exposing the console beyond localhost, read
|
|
78
96
|
[docs/security.md](docs/security.md): a signed-in user can redeploy services.
|
|
@@ -89,7 +107,7 @@ git clone https://github.com/lpalbou/AbstractUIC.git abstractuic
|
|
|
89
107
|
git clone https://github.com/lpalbou/AbstractContinuum.git abstractcontinuum
|
|
90
108
|
cd abstractcontinuum
|
|
91
109
|
npm install
|
|
92
|
-
npm run dev # vite dev server on :
|
|
110
|
+
npm run dev # vite dev server on :3002 (proxies /api to the gateway)
|
|
93
111
|
npm test # vitest (unit + jsdom component tests)
|
|
94
112
|
npm run build # production build into dist/
|
|
95
113
|
npm start # serve dist/ + the app-origin gateway session proxy on :3002
|
|
@@ -103,7 +121,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
|
|
|
103
121
|
- [docs/getting-started.md](docs/getting-started.md) — first run and first execution
|
|
104
122
|
- [docs/architecture.md](docs/architecture.md) — components, data flow, design boundaries
|
|
105
123
|
- [docs/api.md](docs/api.md) — the gateway API families this app consumes
|
|
106
|
-
- [docs/configuration.md](docs/configuration.md) — environment variables
|
|
124
|
+
- [docs/configuration.md](docs/configuration.md) — server environment variables, gateway settings, browser settings
|
|
125
|
+
- [docs/conventions.md](docs/conventions.md) — the work-item grammar the Board reads
|
|
107
126
|
- [docs/security.md](docs/security.md) — trust model (read before deploying)
|
|
108
127
|
- [docs/faq.md](docs/faq.md) / [docs/troubleshooting.md](docs/troubleshooting.md)
|
|
109
128
|
- [CHANGELOG.md](CHANGELOG.md) — release history
|
package/bin/cli.js
CHANGED
|
@@ -10,52 +10,159 @@
|
|
|
10
10
|
|
|
11
11
|
import * as http from 'http';
|
|
12
12
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
13
|
-
import { homedir } from 'os';
|
|
14
13
|
import { join, extname, dirname } from 'path';
|
|
15
14
|
import { fileURLToPath } from 'url';
|
|
16
15
|
import { createGatewaySessionProxy } from '@abstractframework/app-server';
|
|
17
16
|
import { createHubProxy } from './hub_proxy.js';
|
|
17
|
+
import {
|
|
18
|
+
SETTINGS,
|
|
19
|
+
SOURCE_WORDS,
|
|
20
|
+
apply_session_proxy_gates,
|
|
21
|
+
createLiveSettings,
|
|
22
|
+
createSettingsRoute,
|
|
23
|
+
default_settings_path,
|
|
24
|
+
display_value,
|
|
25
|
+
expand_home,
|
|
26
|
+
parse_args,
|
|
27
|
+
read_settings_file,
|
|
28
|
+
resolve_settings,
|
|
29
|
+
setting_spec,
|
|
30
|
+
update_settings_file,
|
|
31
|
+
} from './settings.js';
|
|
18
32
|
|
|
19
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
34
|
const DIST_DIR = join(__dirname, '..', 'dist');
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
function help_text() {
|
|
37
|
+
const lines = [];
|
|
38
|
+
const col = (left, right) => (left.length >= 30 ? ` ${left}\n${' '.repeat(32)}${right}` : ` ${left.padEnd(30)}${right}`);
|
|
39
|
+
lines.push('Usage:');
|
|
40
|
+
lines.push(' abstractcontinuum [options] start the console server');
|
|
41
|
+
lines.push(' abstractcontinuum config get [setting] show settings and their sources');
|
|
42
|
+
lines.push(' abstractcontinuum config set <setting> <value>');
|
|
43
|
+
lines.push(' abstractcontinuum config set hub_token --from-file <path>');
|
|
44
|
+
lines.push(' abstractcontinuum config unset <setting>');
|
|
45
|
+
lines.push(' abstractcontinuum config path print the settings file path');
|
|
46
|
+
lines.push('');
|
|
47
|
+
lines.push('Serves the AbstractContinuum console and proxies /api/* to a Run Gateway');
|
|
48
|
+
lines.push('through a same-origin session proxy.');
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('Options (each is also a saved setting; its name follows "setting"):');
|
|
51
|
+
for (const spec of SETTINGS) {
|
|
52
|
+
const left = spec.type === 'bool' ? spec.flag : `${spec.flag} ${spec.metavar}`;
|
|
53
|
+
const def = spec.type === 'bool' ? 'off' : spec.default === '' ? 'none' : String(spec.default);
|
|
54
|
+
lines.push(col(left, spec.help));
|
|
55
|
+
lines.push(`${' '.repeat(32)}default ${def}; setting ${spec.key}`);
|
|
56
|
+
if (spec.key === 'hub_token') {
|
|
57
|
+
lines.push(col('--hub-token-file <path>', 'read the hub token from a file (never printed)'));
|
|
58
|
+
lines.push(`${' '.repeat(32)}setting hub_token`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push(col('--settings-file <path>', 'settings file'));
|
|
62
|
+
lines.push(`${' '.repeat(32)}default ~/.abstractcontinuum/settings.json`);
|
|
63
|
+
lines.push(col('--help', 'show this help'));
|
|
64
|
+
lines.push(col('--version', 'print the version'));
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push('On/off options take --option, --option=off or --no-option.');
|
|
67
|
+
lines.push('Precedence: launch flag > settings file > environment (legacy) > default.');
|
|
68
|
+
lines.push('The hub seat can also be set on the Settings page.');
|
|
69
|
+
lines.push('Legacy fallback: the environment variables listed in the configuration guide are');
|
|
70
|
+
lines.push('still read when neither a flag nor a setting is set.');
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('Documentation: https://github.com/lpalbou/AbstractContinuum#readme');
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const parsed = parse_args(process.argv.slice(2));
|
|
77
|
+
if (parsed.error) {
|
|
78
|
+
console.error(`abstractcontinuum: ${parsed.error}`);
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
if (parsed.command === 'version') {
|
|
24
82
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
83
|
console.log(pkg.version);
|
|
26
84
|
process.exit(0);
|
|
27
85
|
}
|
|
28
|
-
if (
|
|
29
|
-
console.log(
|
|
30
|
-
|
|
31
|
-
Serves the AbstractContinuum console and proxies /api/* to a Run Gateway
|
|
32
|
-
through a same-origin session proxy. Configuration is environment-driven:
|
|
33
|
-
|
|
34
|
-
PORT HTTP port (default 3002)
|
|
35
|
-
HOST bind address (default 127.0.0.1)
|
|
36
|
-
ABSTRACTCONTINUUM_GATEWAY_URL gateway URL (fallback ABSTRACTGATEWAY_URL,
|
|
37
|
-
default http://127.0.0.1:8080)
|
|
38
|
-
ABSTRACTCONTINUUM_HUB_URL agora hub for the Team page (fallback
|
|
39
|
-
AGORA_HUB_URL, default http://127.0.0.1:8765)
|
|
40
|
-
ABSTRACTCONTINUUM_HUB_SEAT hub seat the Team page acts as (default laurent)
|
|
41
|
-
ABSTRACTCONTINUUM_HUB_KEYS hub key store (default ~/.agora/keys.json)
|
|
42
|
-
ABSTRACTCONTINUUM_HUB_KEY hub API key (overrides the key store)
|
|
43
|
-
|
|
44
|
-
Documentation: https://github.com/lpalbou/AbstractContinuum#readme`);
|
|
86
|
+
if (parsed.command === 'help') {
|
|
87
|
+
console.log(help_text());
|
|
45
88
|
process.exit(0);
|
|
46
89
|
}
|
|
47
90
|
|
|
48
|
-
const
|
|
91
|
+
const SETTINGS_PATH = parsed.settings_file || default_settings_path();
|
|
92
|
+
|
|
93
|
+
if (parsed.command === 'config') {
|
|
94
|
+
process.exit(run_config(parsed));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `abstractcontinuum config get|set|unset|path` — same keys as the flags. */
|
|
98
|
+
function run_config(p) {
|
|
99
|
+
const { action, key, value } = p.config;
|
|
100
|
+
try {
|
|
101
|
+
if (action === 'path') {
|
|
102
|
+
console.log(SETTINGS_PATH);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
if (action === 'set' || action === 'unset') {
|
|
106
|
+
let v = action === 'unset' ? null : value;
|
|
107
|
+
if (action === 'set' && p.from_file) {
|
|
108
|
+
v = readFileSync(p.from_file, 'utf8').trim();
|
|
109
|
+
if (!v) throw new Error(`${p.from_file} is empty`);
|
|
110
|
+
}
|
|
111
|
+
update_settings_file(SETTINGS_PATH, key, v);
|
|
112
|
+
const spec = setting_spec(key);
|
|
113
|
+
const shown = action === 'unset' ? '(removed)' : display_value(spec, read_settings_file(SETTINGS_PATH)[key]);
|
|
114
|
+
console.log(`${key} = ${shown} (${SETTINGS_PATH})`);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
// get
|
|
118
|
+
const { settings, problems } = resolve_settings({ flags: p.flags, file: read_settings_file(SETTINGS_PATH), env: process.env });
|
|
119
|
+
for (const msg of problems) console.error(`warning: ${msg}`);
|
|
120
|
+
if (key) {
|
|
121
|
+
console.log(display_value(setting_spec(key), settings[key].value));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const width = Math.max(...SETTINGS.map((s) => s.key.length)) + 2;
|
|
125
|
+
for (const spec of SETTINGS) {
|
|
126
|
+
const { value: v, source } = settings[spec.key];
|
|
127
|
+
console.log(`${spec.key.padEnd(width)}${display_value(spec, v)} (${SOURCE_WORDS[source]})`);
|
|
128
|
+
}
|
|
129
|
+
console.log(`\nsettings file: ${SETTINGS_PATH}`);
|
|
130
|
+
return 0;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error(`abstractcontinuum config: ${e && e.message ? e.message : e}`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A corrupt settings file stops the start (running on silent defaults
|
|
138
|
+
// would, e.g., drop a saved seat without a word).
|
|
139
|
+
try {
|
|
140
|
+
read_settings_file(SETTINGS_PATH);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error(`abstractcontinuum: cannot read the settings file ${SETTINGS_PATH}: ${e && e.message ? e.message : e}`);
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const settings = createLiveSettings({
|
|
147
|
+
flags: parsed.flags,
|
|
148
|
+
settingsPath: SETTINGS_PATH,
|
|
149
|
+
env: process.env,
|
|
150
|
+
onProblem: (msg) => console.error(`warning: ${msg}`),
|
|
151
|
+
});
|
|
152
|
+
const AT_START = settings.get();
|
|
153
|
+
|
|
154
|
+
const PORT = AT_START.port.value;
|
|
49
155
|
// Default bind is LOOPBACK (entity's c1768 SSRF finding against the shared
|
|
50
156
|
// session proxy, plus a continuum-specific amplifier: this server also
|
|
51
157
|
// mounts the hub proxy carrying the OPERATOR's seat key — a LAN peer
|
|
52
158
|
// reaching the port could author hub messages as the operator). Wider
|
|
53
|
-
// binds are an explicit deployment choice
|
|
54
|
-
const HOST =
|
|
55
|
-
const DEFAULT_GATEWAY_URL =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
159
|
+
// binds are an explicit deployment choice (--host).
|
|
160
|
+
const HOST = AT_START.host.value;
|
|
161
|
+
const DEFAULT_GATEWAY_URL = AT_START.gateway_url.value;
|
|
162
|
+
|
|
163
|
+
// The session proxy reads its hardening gates from the environment only;
|
|
164
|
+
// hand it the resolved values (flag > setting > environment > default).
|
|
165
|
+
apply_session_proxy_gates(AT_START, process.env);
|
|
59
166
|
|
|
60
167
|
const MIME_TYPES = {
|
|
61
168
|
'.html': 'text/html',
|
|
@@ -120,8 +227,8 @@ function serveFile(res, filePath) {
|
|
|
120
227
|
}
|
|
121
228
|
}
|
|
122
229
|
|
|
123
|
-
// Cookies abstractcontinuum_gateway_
|
|
124
|
-
//
|
|
230
|
+
// Cookies abstractcontinuum_gateway_* and header x-abstractcontinuum-csrf
|
|
231
|
+
// derive from appId.
|
|
125
232
|
const gatewaySessionProxy = createGatewaySessionProxy({
|
|
126
233
|
appId: 'abstractcontinuum',
|
|
127
234
|
defaultGatewayUrl: DEFAULT_GATEWAY_URL,
|
|
@@ -130,7 +237,18 @@ const gatewaySessionProxy = createGatewaySessionProxy({
|
|
|
130
237
|
// Team page transport: /api/hub/* forwards an allowlisted hub API subset
|
|
131
238
|
// with the OPERATOR seat's key attached server-side (proposal c1692,
|
|
132
239
|
// agency contract c1696 — authorship stays the operator's own).
|
|
133
|
-
|
|
240
|
+
// Every hub setting is a getter: a seat saved on the Settings page or with
|
|
241
|
+
// `abstractcontinuum config set` applies without a restart.
|
|
242
|
+
const hubProxy = createHubProxy({
|
|
243
|
+
hubUrl: () => settings.value('hub_url'),
|
|
244
|
+
seat: () => settings.value('hub_seat'),
|
|
245
|
+
keysPath: () => expand_home(settings.value('hub_keys')),
|
|
246
|
+
token: () => settings.value('hub_token'),
|
|
247
|
+
allowRemote: () => settings.value('hub_allow_remote'),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// The Settings page's door to this server's own settings (hub seat).
|
|
251
|
+
const settingsRoute = createSettingsRoute({ live: settings });
|
|
134
252
|
|
|
135
253
|
const server = http.createServer((req, res) => {
|
|
136
254
|
// Parse against a FIXED base: a malformed Host header (e.g. "a b") makes
|
|
@@ -149,6 +267,10 @@ const server = http.createServer((req, res) => {
|
|
|
149
267
|
return;
|
|
150
268
|
}
|
|
151
269
|
|
|
270
|
+
if (settingsRoute.handle(req, res, pathname)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
152
274
|
if (hubProxy.handle(req, res, pathname, search)) {
|
|
153
275
|
return;
|
|
154
276
|
}
|
|
@@ -195,10 +317,13 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
195
317
|
});
|
|
196
318
|
|
|
197
319
|
server.listen(PORT, HOST, () => {
|
|
320
|
+
const seat = AT_START.hub_seat;
|
|
198
321
|
console.log(`
|
|
199
322
|
AbstractContinuum is running.
|
|
200
|
-
Local:
|
|
201
|
-
Gateway:
|
|
323
|
+
Local: http://localhost:${PORT}
|
|
324
|
+
Gateway: ${DEFAULT_GATEWAY_URL}
|
|
325
|
+
Hub seat: ${seat.value} (${SOURCE_WORDS[seat.source]})
|
|
326
|
+
Settings: ${SETTINGS_PATH}
|
|
202
327
|
`);
|
|
203
328
|
});
|
|
204
329
|
|
package/bin/hub_proxy.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Serves /api/hub/* by forwarding an ALLOWLISTED subset of the hub HTTP
|
|
6
6
|
* API with the OPERATOR seat's key attached server-side. The key comes
|
|
7
|
-
* from the operator's standard key store (~/.agora/keys.json) or
|
|
7
|
+
* from the operator's standard key store (~/.agora/keys.json) or the hub_token setting; it
|
|
8
8
|
* never appears in the browser, a repo, or a URL (the c1556 rule
|
|
9
9
|
* generalized from charter reads to the whole chat surface).
|
|
10
10
|
*
|
|
11
11
|
* Contract rules honored here (agency c1696):
|
|
12
12
|
* - Only allowlisted paths forward — the proxy is a door, not a tunnel.
|
|
13
13
|
* - GET /api/hub/meta names the seat so the UI can render authorship
|
|
14
|
-
* honestly ("posting as
|
|
14
|
+
* honestly ("posting as <seat>").
|
|
15
15
|
* - Ack/read_message are ordinary POSTs the UI calls on EXPLICIT read
|
|
16
16
|
* only (never on render) — enforced UI-side, but the proxy exposes
|
|
17
17
|
* them as distinct paths so an audit can see which fired.
|
|
@@ -214,10 +214,10 @@ function origin_allowed(req) {
|
|
|
214
214
|
return Boolean(host) && parsed.host.toLowerCase() === host;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function read_seat_key(keys_path, hub_url, seat) {
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
if (
|
|
217
|
+
function read_seat_key(keys_path, hub_url, seat, token) {
|
|
218
|
+
// An explicit token (--hub-token / --hub-token-file / the hub_token
|
|
219
|
+
// setting, resolved by bin/settings.js) wins over the key store.
|
|
220
|
+
if (token) return token;
|
|
221
221
|
try {
|
|
222
222
|
const store = JSON.parse(readFileSync(keys_path, 'utf8'));
|
|
223
223
|
const entry = store[`${hub_url}::${seat}`];
|
|
@@ -229,14 +229,32 @@ function read_seat_key(keys_path, hub_url, seat) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/** A value or a getter: the server hands getters so a seat saved on the
|
|
233
|
+
* Settings page (or with `abstractcontinuum config set`) applies without
|
|
234
|
+
* a restart. */
|
|
235
|
+
function live(v) {
|
|
236
|
+
return typeof v === 'function' ? v() : v;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* opts: hubUrl, seat, keysPath, token (seat API key; overrides the key
|
|
241
|
+
* store), allowRemote (boolean) — each a value or a getter. Settings are
|
|
242
|
+
* resolved by bin/settings.js (flag > setting > environment > default);
|
|
243
|
+
* this module reads no environment of its own.
|
|
244
|
+
*/
|
|
232
245
|
export function createHubProxy(opts) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
246
|
+
const cfg = () => ({
|
|
247
|
+
hub_url: String(live(opts.hubUrl) || '').replace(/\/+$/, ''),
|
|
248
|
+
seat: String(live(opts.seat) || '').trim(),
|
|
249
|
+
keys_path: String(live(opts.keysPath) || ''),
|
|
250
|
+
token: String(live(opts.token) || '').trim(),
|
|
251
|
+
allow_remote: live(opts.allowRemote) === true,
|
|
252
|
+
});
|
|
236
253
|
|
|
237
254
|
/** Lazily re-read per request: key rotation must not need a restart. */
|
|
238
255
|
function key() {
|
|
239
|
-
|
|
256
|
+
const c = cfg();
|
|
257
|
+
return read_seat_key(c.keys_path, c.hub_url, c.seat, c.token);
|
|
240
258
|
}
|
|
241
259
|
|
|
242
260
|
async function forward(req, res, hub_path, search) {
|
|
@@ -290,7 +308,8 @@ export function createHubProxy(opts) {
|
|
|
290
308
|
const seat_key = key();
|
|
291
309
|
if (!seat_key && !route.keyless) {
|
|
292
310
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
293
|
-
|
|
311
|
+
const c = cfg();
|
|
312
|
+
res.end(JSON.stringify({ error: 'hub_seat_unavailable', detail: `No hub key for seat "${c.seat}" in ${c.keys_path}. Provision the seat there, or start Continuum with --hub-token-file <path>.` }));
|
|
294
313
|
return;
|
|
295
314
|
}
|
|
296
315
|
|
|
@@ -344,7 +363,7 @@ export function createHubProxy(opts) {
|
|
|
344
363
|
// metadata). Every other write: application/json.
|
|
345
364
|
upstream_headers['Content-Type'] = route.upload ? String(req.headers['content-type'] || 'application/octet-stream') : 'application/json';
|
|
346
365
|
}
|
|
347
|
-
const r = await fetch(`${hub_url}${hub_path}${search || ''}`, {
|
|
366
|
+
const r = await fetch(`${cfg().hub_url}${hub_path}${search || ''}`, {
|
|
348
367
|
method: req.method,
|
|
349
368
|
headers: upstream_headers,
|
|
350
369
|
body: body && req.method !== 'GET' ? body : undefined,
|
|
@@ -415,7 +434,7 @@ export function createHubProxy(opts) {
|
|
|
415
434
|
// as the operator seat (adversary P0).
|
|
416
435
|
const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
|
|
417
436
|
const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
|
|
418
|
-
const allow_remote =
|
|
437
|
+
const allow_remote = cfg().allow_remote;
|
|
419
438
|
const seat_key = key();
|
|
420
439
|
let WebSocketServer, WebSocket;
|
|
421
440
|
try {
|
|
@@ -433,7 +452,7 @@ export function createHubProxy(opts) {
|
|
|
433
452
|
// never negotiated (live find: browsers/ws clients offer it by
|
|
434
453
|
// default and the pipe corrupts).
|
|
435
454
|
if (!this._wss) this._wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
|
436
|
-
const hub_ws_url = hub_url.replace(/^http/, 'ws') + '/ws';
|
|
455
|
+
const hub_ws_url = cfg().hub_url.replace(/^http/, 'ws') + '/ws';
|
|
437
456
|
const upstream = new WebSocket(hub_ws_url, {
|
|
438
457
|
headers: { Authorization: `Bearer ${seat_key}` },
|
|
439
458
|
perMessageDeflate: false,
|
|
@@ -521,10 +540,10 @@ export function createHubProxy(opts) {
|
|
|
521
540
|
// this is defense-in-depth if HOST is widened deliberately.
|
|
522
541
|
const peer = String(req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : '');
|
|
523
542
|
const loopback = peer === '127.0.0.1' || peer === '::1' || peer === '::ffff:127.0.0.1' || peer === '';
|
|
524
|
-
const allow_remote =
|
|
543
|
+
const allow_remote = cfg().allow_remote;
|
|
525
544
|
if (!loopback && !allow_remote) {
|
|
526
545
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
527
|
-
res.end(JSON.stringify({ error: 'hub_proxy_non_loopback', detail: `Hub proxy refuses non-loopback peer ${peer} (it authors as the operator seat).
|
|
546
|
+
res.end(JSON.stringify({ error: 'hub_proxy_non_loopback', detail: `Hub proxy refuses non-loopback peer ${peer} (it authors as the operator seat). Start Continuum with --hub-allow-remote only behind a trusted front.` }));
|
|
528
547
|
return true;
|
|
529
548
|
}
|
|
530
549
|
// Cross-origin browser requests are refused even from loopback: the
|
|
@@ -538,7 +557,8 @@ export function createHubProxy(opts) {
|
|
|
538
557
|
|
|
539
558
|
if (pathname === '/api/hub/meta') {
|
|
540
559
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
541
|
-
|
|
560
|
+
const c = cfg();
|
|
561
|
+
res.end(JSON.stringify({ ok: true, hub_url: c.hub_url, seat: c.seat, seat_key_present: Boolean(key()) }));
|
|
542
562
|
return true;
|
|
543
563
|
}
|
|
544
564
|
const hub_path = pathname.slice('/api/hub'.length);
|